﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Runtime.Serialization.Formatters.Binary;
using System.Runtime.Serialization;
using System.IO;
using System.Net;
using System.Windows.Forms;
using Microsoft.Win32;
using System.Net.NetworkInformation;
using PMC.Properties;
using HtmlDocument = HtmlAgilityPack.HtmlDocument;

namespace PMC
{
    public static class Global
    {
        public static Color LogMessage = Color.White;
        public static Color LogWarning = Color.Yellow;
        public static Color LogError = Color.Red;
        public const string LogFormat = "[{0}] {1} | {2}";
        public const string DefaultNamespace = "Default";

        #region Deserialize
        /// <summary>
        /// Deserialize from binary
        /// </summary>
        /// <param name="path">Path and filename</param>
        /// <returns>Returns the read object</returns>
        public static T Deserialize<T>(string path)
        {
            FileStream fs = null;
            try
            {
                if (File.Exists(path))
                {
                    fs = new FileStream(path, FileMode.Open);
                    BinaryFormatter bin = new BinaryFormatter();
                    return (T)bin.Deserialize(fs);
                }
                throw new FileNotFoundException("File not found or inaccessible " + path);
            }
            catch (Exception e)
            {
                Log.Instance.AddException(e);
                throw;
            }
            finally
            {
                if (fs != null)
                    fs.Close();
            }
        }

        /// <summary>
        /// Deserialize XML file to the given type
        /// </summary>
        /// <typeparam name="T">Type of object</typeparam>
        /// <param name="path">Path of the file to deserialize</param>
        /// <returns>Object that was stored in the file</returns>
        public static T XMLDeserialize<T>(string path)
        {
            if (!File.Exists(path)) //TODO
                return default(T);

            var serializer = new DataContractSerializer(typeof(T), null, 0x7FFF, false, true, null);
            using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read))
            {
                var obj = (T) serializer.ReadObject(stream);
                return obj;
            }
        }
        #endregion

        #region Serialize
        /// <summary>
        /// Serialize to binary
        /// </summary>
        /// <param name="path">Path and filename</param>
        /// <param name="o">Object to serialize</param>
        /// <returns>Wether serializing was succesfull</returns>
        public static bool Serialize(string path, object o)
        {
            try
            {
                using (var fs = new FileStream(path, FileMode.OpenOrCreate))
                {
                    var bin = new BinaryFormatter();
                    bin.Serialize(fs, o);
                    return true;
                }
            }
            catch (Exception e)
            {
                Log.Instance.AddException(e);
                throw;
            }
        }

        /// <summary>
        /// Serialize an object to the given path with an XML layout
        /// </summary>
        /// <param name="path">Path to save the file to</param>
        /// <param name="o">Object to write</param>
        public static void XMLSerialize(string path, object o)
        {
            if (!Directory.Exists(Directory.GetParent(path).Name))
                Directory.CreateDirectory(Directory.GetParent(path).Name);

            var serializer = new DataContractSerializer(o.GetType(), null, 0x7FFF, false, true, null);
            using (var stream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.Write))
            {
                serializer.WriteObject(stream, o);
            }
        }
        #endregion

        #region Encrypt

        /// <summary>
        /// Encrypt a string
        /// </summary>
        /// <remarks>This function should only be used for encryption to create unreadable strings. 
        /// This method is <b>NOT</b> intented for powerfull decryption of sensitive information!</remarks>
        /// <seealso cref="http://stackoverflow.com/questions/6358254/how-do-i-remove-net-cryptostream-padding"/>
        /// <param name="plainText"></param>
        /// <returns></returns>
        public static string EncryptString(string plainText)
        {
            var clearTextBytes = Encoding.UTF8.GetBytes(plainText);

            using (SymmetricAlgorithm rijn = SymmetricAlgorithm.Create())
            {

                using (var ms = new MemoryStream())
                {
                    var rgbIV = Encoding.ASCII.GetBytes("ryojvlzmdalyglrj");
                    var key = Encoding.ASCII.GetBytes("hcxilkqbbhczfeultgbskdmaunivmfuo");
                    var encryptor = rijn.CreateEncryptor(key, rgbIV);
                    using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write))
                    {
                        cs.Write(clearTextBytes, 0, clearTextBytes.Length);
                    }
                    return Convert.ToBase64String(ms.ToArray());
                }
            }
        }

        #endregion

        #region Decrypt

        /// <summary>
        /// Decrypt a string
        /// </summary>
        /// <seealso cref="http://stackoverflow.com/questions/6358254/how-do-i-remove-net-cryptostream-padding"/>
        /// <param name="encryptedText"></param>
        /// <returns></returns>
        public static string DecryptString(string encryptedText)
        {
            try
            {
                var encryptedTextBytes = Convert.FromBase64String(encryptedText);

                using (var ms = new MemoryStream())
                {
                    using (var rijn = SymmetricAlgorithm.Create())
                    {
                        var rgbIV = Encoding.ASCII.GetBytes("ryojvlzmdalyglrj");
                        var key = Encoding.ASCII.GetBytes("hcxilkqbbhczfeultgbskdmaunivmfuo");
                        var decryptor = rijn.CreateDecryptor(key, rgbIV);

                        using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Write))
                        {
                            cs.Write(encryptedTextBytes, 0, encryptedTextBytes.Length);
                        }
                        return Encoding.UTF8.GetString(ms.ToArray());
                    }
                }
            }
            catch (Exception e)
            {
                Log.Instance.AddException(e);
                throw;
            }
        }

        #endregion

        private const char ExplodeChar = '*';
        private const int devideFactor = 2;
        private const int minNumber = 15;

        #region Decrypt Password
        /// <summary>
        /// Decrypt passwords from older versions
        /// </summary>
        /// <remarks>
        /// This method should only be used for upgrading from older versions, hench the internal modifier!
        /// </remarks>
        /// <param name="encrypteddata"></param>
        /// <returns></returns>
        [Obsolete("Use Global.DecryptString for decrypting passwords and such!")]
        internal static string DecryptPassword(string encrypteddata)
        {
            for (int i = 0; i < 2; i++)
            {
                string[] splitData = encrypteddata.Split(new[] { ExplodeChar }, StringSplitOptions.RemoveEmptyEntries);
                encrypteddata = "";
                foreach (string str in splitData)
                {
                    int s = Convert.ToInt32(str, CultureInfo.InvariantCulture) / devideFactor - minNumber;
                    encrypteddata += Convert.ToChar(s);
                }
            }

            return encrypteddata;
        }
        #endregion

        public static string AssemblyDirectory
        {
            get
            {
                string codeBase = Assembly.GetEntryAssembly().CodeBase;
                var uri = new UriBuilder(codeBase);
                string path = Uri.UnescapeDataString(uri.Path);
                return Path.GetDirectoryName(path);
            }
        }

        private static readonly string CookieCSerialPath = Settings.Default.SettingsFolder + "/CookieContainer.bin";
        private static CookieContainer container;
        /// <summary>
        /// Global CookieContainer
        /// </summary>
        public static CookieContainer CookieJar
        {
            get
            {
                if (container != null)
                    return container;

                // Save CookieJar to the settings folder
                Application.ApplicationExit += (target, e) => Serialize(CookieCSerialPath, container); //TODO check if this works correctly!

                if (File.Exists(CookieCSerialPath))
                {
                    try
                    {
                        container = Deserialize<CookieContainer>(CookieCSerialPath);
                    }
                    catch (Exception e)
                    {
                        Log.Instance.AddException(e);
                    }
                }

                return container ?? (container = new CookieContainer());
            }
        }

        /// <summary>
        /// Creates valid filenames
        /// </summary>
        /// <param name="name">Name to make valid</param>
        /// <param name="lower">Convert filename to all lowercase characters</param>
        /// <returns></returns>
        public static string MakeValidFileName(string name, bool lower = false)
        {
            foreach (var invalid in Path.GetInvalidFileNameChars())
            {
                name = name.Replace(Convert.ToString(invalid, CultureInfo.InvariantCulture), string.Empty);
            }
            return lower ? name.ToLower(CultureInfo.InvariantCulture) : name;
        }

        public abstract class HtmlUtility
        {
            /// <summary>
            /// Performs a HttpWebRequest and interpret the body as an System.Drawing.Image
            /// </summary>
            /// <param name="url">Url to visit</param>
            /// <returns>Url to System.Drawing.Image</returns>
            public static Image ImageFromUrl(Uri url)
            {
                try
                {
                    using (var response = HttpWebRequest(url))
                    {
                        using (var stream = response.GetResponseStream())
                        {
                            if (stream != null)
                            {
                                return Image.FromStream(stream);
                            }
                        }
                    }
                }
                catch (Exception e)
                {
                    Log.Instance.AddException(e);
                }
                return null;
            }

            /// <summary>
            /// Converts a string to a HtmlAgilityPack.HtmlDocument
            /// </summary>
            /// <param name="document">Html of a website</param>
            /// <returns>document as HtmlAgilityPack.HtmlDocument</returns>
            public static HtmlDocument StringToHtmlDocument(String document)
            {
                var doc = new HtmlDocument();
                doc.LoadHtml(document);
                return doc;
            }

            /// <summary>
            /// Performs a C#-equivalent to a XMLHttpRequest
            /// </summary>
            /// <remarks>Don't forget to close the HttpWebResponse-stream!</remarks>
            /// <param name="url"></param>
            /// <param name="method"></param>
            /// <param name="postData"></param>
            /// <returns>Responsestream</returns>
            public static HttpWebResponse HttpWebRequest(Uri url, string method = "GET", string postData = "")
            {
                try
                {
                    var webRequest = (HttpWebRequest)WebRequest.Create(url);
                    webRequest.CookieContainer = CookieJar;
                    webRequest.Method = method;

                    if (method == "POST")
                    {
                        webRequest.ContentType = "application/x-www-form-urlencoded";
                        using (var requestStream = webRequest.GetRequestStream())
                        {
                            using (var postDataWriter = new StreamWriter(requestStream))
                            {
                                postDataWriter.Write(postData ?? string.Empty);
                            }
                        }
                    }
                    // This response needs to be closed manually by the callers
                    var resp = (HttpWebResponse)webRequest.GetResponse();
                    CookieJar.Add(resp.Cookies);
                    return resp;
                }
                catch (Exception exception)
                {
                    Log.Instance.AddMessage(string.Format("Url: {0}, Method: {1}, PostData: {2}", url, method, postData));
                    Log.Instance.AddMessage(exception);
                }
                return null;
            }

            /// <summary>
            /// Performs a HttpWebRequest and returns the response body as HtmlAgilityPack.HtmlDocument
            /// </summary>
            /// <param name="url">Url to visit</param>
            /// <param name="method">HTTP method</param>
            /// <param name="postData">Data to post</param>
            /// <returns>Body as HtmlAgilityPack.HtmlDocument</returns>
            public static HtmlDocument HttpPageRequest(Uri url, string method = "GET", string postData = "")
            {
                using (var info = HttpWebRequest(url, method, postData))
                {
                    if (info != null)
                    {
                        using (var responseStream = info.GetResponseStream())
                        {
                            if (responseStream != null)
                            {
                                using (var str = new StreamReader(responseStream))
                                {
                                    return StringToHtmlDocument(str.ReadToEnd());
                                }
                            }
                        }
                    }
                }
                return null;
            }

            #region (Obsolete) Cookie Methods
            /// <summary>
            /// Converts a string (HTTP-Cookie header) into a CookieCollection
            /// </summary>
            /// <param name="cookieheader">String to parse</param>
            /// <param name="url">Url of the corresponding website</param>
            /// <returns></returns>
            [Obsolete("Not needed anymore with the new WBrowserWebRequest")]
            public static CookieCollection CookieHeaderToCookieCollection(string cookieheader, Uri url)
            {
                var returnCollection = new CookieCollection();
                foreach (var cookies in cookieheader.Split(new[] { "; " }, StringSplitOptions.RemoveEmptyEntries))
                {
                    var split = cookies.Trim().Split(new[] { "=" }, StringSplitOptions.RemoveEmptyEntries);
                    if (split.Length == 2)
                        returnCollection.Add(new Cookie(split[0], split[1], url.LocalPath, url.Host));
                }
                return returnCollection;
            }

            /// <summary>
            /// Converts an CookieCollection into a string that is suitable for a HTTP-Cookie header
            /// </summary>
            /// <param name="collection">CookieCollection to convert</param>
            /// <param name="url">Url of the collection</param>
            /// <returns>String representing a HTTP-Cookie header</returns>
            [Obsolete("Not needed anymore with the new WBrowserWebRequest")]
            public static string CookieCollectionToCookieHeader(CookieCollection collection, Uri url)
            {
                var tempContainer = new CookieContainer();
                tempContainer.Add(collection);
                return tempContainer.GetCookieHeader(url);
            }

            /// <summary>
            /// Returns all cookies that the CookieContainer is holding
            /// </summary>
            /// <param name="cc">CookieContainer</param>
            /// <returns>List of cookies</returns>
            [Obsolete("Not needed anymore with the new WBrowserWebRequest")]
            public static List<Cookie> GetAllCookies(CookieContainer cc)
            {
                Hashtable table = (Hashtable)cc.GetType().InvokeMember("m_domainTable", BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.Instance, null, cc, new object[] { });

                return (
                           from object pathList
                               in table.Values
                           select (SortedList)pathList.GetType().InvokeMember("m_list", BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.Instance, null, pathList, new object[] { })
                               into lstCookieCol
                               from CookieCollection colCookies
                                   in lstCookieCol.Values
                               from Cookie c
                                   in colCookies
                               select c
                       ).ToList();
            } 
            #endregion
        }
    
        /// <summary>
        /// Checks wether internet connection is available
        /// </summary>
        /// <seealso cref="http://stackoverflow.com/questions/520347/c-sharp-how-do-i-check-for-a-network-connection"/>
        /// <returns>Boolean representing if an working networkconnection is available.</returns>
        public static bool CheckInternetConnection()
        {
            const long minimumSpeed = 10000000;

            return NetworkInterface.GetIsNetworkAvailable() &&
                NetworkInterface.GetAllNetworkInterfaces()
                .Where(ni => (ni.OperationalStatus == OperationalStatus.Up) && (ni.NetworkInterfaceType != NetworkInterfaceType.Loopback) && (ni.NetworkInterfaceType != NetworkInterfaceType.Tunnel))
                .Where(ni => ni.Speed >= minimumSpeed)
                .Any(ni => (ni.Description.IndexOf("virtual", StringComparison.OrdinalIgnoreCase) < 0) && (ni.Name.IndexOf("virtual", StringComparison.OrdinalIgnoreCase) < 0));
        }

        #region (Obsolete) IE Protected Mode Methods
        /// <summary>
        /// Enables/disables protected mode for IE
        /// </summary>
        /// <param name="enable"><b>System.Boolean</b> representing wether the protected mode should be enabled</param>
        [Obsolete("Not needed anymore with the new WBrowserWebRequest")]
        public static void SetIEProtectedMode(bool enable)
        {
            var key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\3\");
            if (key == null)
                throw new NullReferenceException("Subkey not exits");

            key.SetValue("2500", enable ? 0 : 3);
        }

        /// <summary>
        /// Gets wether protected mode is enabled for IE
        /// </summary>
        /// <returns></returns>
        [Obsolete("Not needed anymore with the new WBrowserWebRequest")]
        public static bool GetIEProtectedMode()
        {
            var key = Registry.CurrentUser.CreateSubKey(@"Software\Microsoft\Windows\CurrentVersion\Internet Settings\Zones\3\");
            var value = key.GetValue("2500");
            return (key != null && value != null) && (int)value == 0;
        } 
        #endregion

        /// <summary>
        /// Get build date from Assembly version
        /// </summary>
        /// <remarks>Assembly must have a wildcard like this: AssemblyVersion("1.0.*"), otherwise the result will return an incorrect DateTime</remarks>
        /// <param name="a">Assembly to get the build-date from</param>
        /// <returns>System.DateTime representing build date from Assembly</returns>
        public static DateTime ExtractDateTime(this Assembly a)
        {
            Version v = a.GetName().Version;
            return new DateTime(2000, 1, 1, 0, 0, 0)
                .AddDays(v.Build)
                .AddHours(23)
                .AddMinutes(59)
                .AddSeconds(59);
        }

        public static void ForEach<T>(this IEnumerable<T> elements, Action<T> action)
        {
            if (elements == null)
                throw new ArgumentNullException("elements");

            foreach (var element in elements)
            {
                action.Invoke(element);
            }
        }
    }
}
